/*
 * Copyright 2009-2010 Nanjing RedOrange ltd (http://www.red-orange.cn)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package redora.generator;

import org.jetbrains.annotations.NotNull;
import org.modeshape.common.text.Inflector;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import redora.util.BusinessRuleMessageUtils;
import redora.util.DocPrint;

import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import java.util.ArrayList;
import java.util.Set;

import static javax.xml.xpath.XPathConstants.NODESET;
import static org.apache.commons.lang.StringUtils.*;
import static redora.generator.XMLUtil.*;

/**
 * There are default settings for a model that, because they are default, are not available in the model file.
 * The Normalizer fixes this. So, if an attribute is omitted in model file, here they will be inserted with the necessary
 * default value.
 * Also, here a few helper stuff is added to make the templates easier. For example the object name. Here is the list:<br>
 * - object.name;
 *
 * @author Nanjing RedOrange (http://www.red-orange.cn)
 */
public class Normalizer {

    String modelName;
    final String packageName;
    Document doc;

    /**
     * @param packageName Add the object/package to the model
     */
    public Normalizer(@NotNull String packageName) {
        this.packageName = packageName;
    }

    /**
     * Enhances the model XML document with name, package and sequence tags and then invokes.
     * More normalization stuff that will fill in all defaulted (and left out) attributes.
     * After running the generator look in your ~/redora directory. There are the normalized
     * model files located. Compare them with the original model files to see what is happening here.
     *
     * @param doc          The loaded Document with the model
     * @param modelName    The file name is used as model name and insert object/name to the model
     * @param sortedModels List of models that have the sorted = true tag
     * @param sequence     Simple incremental sequence. Used to uniquely identify (minimalist) the table alias in queries
     * @throws ModelGenerationException Wrapper on all throw exceptions
     */
    public void normalize(@NotNull Document doc, @NotNull String modelName, @NotNull Set<String> sortedModels, int sequence)
            throws ModelGenerationException {
        this.doc = doc;
        this.modelName = modelName;

        // The file name is also the object name!
        attribute(doc.getFirstChild(), "name", modelName);
        addChildElement(doc, "plural", new Inflector().pluralize(modelName));
        addChildElement(doc, "onlyServerSide", "false");
        // The package is for all objects the same and it is set in the
        // maven pom.
        attribute(doc.getFirstChild(), "package", packageName);
        attribute(doc.getFirstChild(), "sequence", String.valueOf(sequence));

        normalizeObject(sortedModels);
        normalizeAttributes(sortedModels);
        normalizeQueries();
        addScope();
        addLazy();
        doc.normalize();
    }

    /**
     * And adds the default elements:<br>
     * - sorted;<br>
     * - trashcan;<br>
     * - finder (findAll);<br>
     * <p/>
     * Adds the sortOrder attribute if this model is sorted.
     * Checks messages for parameters {0}, {1} ..., and add the parameters to the message as
     * params/param tags. Messages can be business rule messages and generic messages.
     *
     * @param sortedModels The list of all models that have the sorted=true tag.
     * @throws ModelGenerationException Wrapper on XPath exception
     */
    void normalizeObject(@NotNull Set<String> sortedModels) throws ModelGenerationException {
        addChildElement(doc, "sorted", "false");
        addChildElement(doc, "trashcan", "false");
        extractMessageParameters("/object/businessRules/businessRule/message");
        extractMessageParameters("/object/messages/messageBundle/message");
        if (sortedModels.contains(modelName)) {
            Node sortNode = doc.createElement("integer");
            attribute(sortNode, "name", "sortOrder");
            attribute(sortNode, "sort", "asc");

            //It should be first in the list. It will get priority in the sort order query.
            Node first;
            try {
                NodeList attributes = attributes(doc, null);
                first = attributes.item(0);
            } catch (XPathExpressionException e) {
                throw new ModelGenerationException("Can't set the object/attributes/sorted node", e);
            }
            doc.getElementsByTagName("attributes").item(0).insertBefore(sortNode,
                    first);
        }

        Node idFinder = addFinder(doc, doc.getFirstChild(), "findById", "false");
        attribute(idFinder, "listName", "FIND_BY_ID_LIST");
        attribute(idFinder, "tableName", "FIND_BY_ID_TABLE");

        Node allFinder = addFinder(doc, doc.getFirstChild(), "findAll", "false");
        attribute(allFinder, "listName", "FIND_ALL_LIST");
        attribute(allFinder, "tableName", "FIND_ALL_TABLE");
    }

    private void extractMessageParameters(@NotNull String query) throws ModelGenerationException {
        XPath xpath = XPathFactory.newInstance().newXPath();
        try {
            NodeList messages = (NodeList)
                    xpath.evaluate(query, doc, NODESET);
            for (int i = 0; i < messages.getLength(); i++) {
                Node message = messages.item(i);
                if (message.getNodeName().equals("message")) {
                    if (message.getAttributes().getNamedItem("language") != null
                            && message.getAttributes().getNamedItem("language").getNodeValue().equals("en")) {
                        String defMesg = message.getTextContent();
                        ArrayList<String> params = new ArrayList<String>();
                        BusinessRuleMessageUtils.getBusinessRuleMessageParams(defMesg, params);
                        if (params.size() > 0) {
                            Node paramsNode = doc.createElement("params");
                            message.getParentNode().appendChild(paramsNode);
                            for (String param : params) {
                                Node paramNode = doc.createElement("param");
                                paramNode.setTextContent(param);
                                paramsNode.appendChild(paramNode);
                            }
                        }
                    }
                }
            }
        } catch (XPathExpressionException e) {
            DocPrint.print(doc);
            throw new ModelGenerationException(
                    "Failed to parse this query " + query, e);
        }
    }

    /**
     * A lot of logic is in the combination of elements and attributes in the
     * model. There are default values that are different for different
     * attributes, and more stuff that makes he modeling language easy for the
     * user, but difficult for the template builder. <br>
     * This function normalizes this. It will fill in optional (default)
     * attributes and add virtual attributes:<br>
     * - lazy;<br>
     * - className;<br>
     * - fieldName;<br>
     * - className and fieldName are derived from class and/or name. Or from the element type itself.
     * - plural;<br>
     * - scope;<br>
     * - multiplicity;<br>
     * - list;<br>
     * - notnull;<br>
     * - cascade;<br>
     * - position;<br>
     * - pigsear;<br>
     * <br>
     * Also elements are added:<br>
     * - finder for object and enum attributes;<br>
     * - [object]Id element for object attribute (ie a relationship column in the DB);<br>
     *
     * @param sortedModels The list of all models that have the sorted=true tag.
     * @throws ModelGenerationException Wrapper on XPath exception
     */
    void normalizeAttributes(@NotNull Set<String> sortedModels) throws ModelGenerationException {
        NodeList attributes;
        try {
            attributes = attributes(doc, null);
        } catch (XPathExpressionException e) {
            DocPrint.print(doc);
            throw new ModelGenerationException("Failed to parse the attributes in this model", e);
        }
        for (int i = 0; i < attributes.getLength(); i++) {
            Node attribute = attributes.item(i);
            if (attribute.getNodeName().equals("datetime")) {
                attribute(attribute, "className", "Date");
            } else if (attribute.getNodeName().equals("enum")) {
                attribute(attribute, "className", attribute.getAttributes().getNamedItem(
                        "class").getNodeValue());
                if (attribute.getAttributes().getNamedItem("name") != null) {
                    attribute(attribute, "fieldName", attribute.getAttributes().getNamedItem("name").getNodeValue());
                } else {
                    attribute(attribute, "fieldName", uncapitalize(attribute.getAttributes().getNamedItem("class").getNodeValue()));
                }
                attribute(attribute, "scope", "local");
                Node finder = addFinder(doc, attribute, "false", false);
                attribute(finder, "name", "findBy" + capitalize(attribute.getAttributes().getNamedItem("fieldName").getNodeValue()));
                String staticName = "FIND_BY";
                for (String part : splitByCharacterTypeCamelCase(attribute.getAttributes().getNamedItem("fieldName").getNodeValue()))
                    staticName += "_" + part.toUpperCase();
                attribute(finder, "listName", staticName + "_LIST");
                attribute(finder, "tableName", staticName + "_TABLE");
            } else if (attribute.getNodeName().equals("html")) {
                attribute(attribute, "className", "String");
                attribute(attribute, "lazy", "true");
            } else if (attribute.getNodeName().equals("object")) {
                String className = attribute.getAttributes().getNamedItem("class").getNodeValue();
                String fieldName = uncapitalize(className);
                if (attribute.getAttributes().getNamedItem("name") != null) {
                    fieldName = attribute.getAttributes().getNamedItem("name").getNodeValue();
                }
                attribute(attribute, "className", className);
                attribute(attribute, "fieldName", fieldName);
                attribute(attribute, "lazy", "true");
                attribute(attribute, "list", "false");
                attribute(attribute, "notnull", "false"); //Needed below
                attribute(attribute, "cascade", "true");
                if (className.equals(modelName)) {
                    attribute(attribute, "pigsear", "true");
                } else {
                    attribute(attribute, "pigsear", "false");
                }
                if (doc.getElementsByTagName(fieldName + "Id").getLength() == 0) {
                    Node longNode = doc.createElement("long");
                    attribute(longNode, "name", fieldName + "Id");
                    attribute(longNode, "lazy", "false");
                    attribute(longNode, "className", "Long");
                    attribute(longNode, "fieldName", fieldName + "Id");
                    attribute(longNode, "notnull", attribute.getAttributes().getNamedItem("notnull").getNodeValue());
                    attribute(longNode, "list", attribute.getAttributes().getNamedItem("list").getNodeValue());
                    attribute(longNode, "parentClass", className);
                    if (className.equals(modelName)) {
                        attribute(longNode, "pigsear", "true");
                    } else {
                        attribute(longNode, "pigsear", "false");
                    }
                    doc.getElementsByTagName("attributes").item(0).insertBefore(longNode,
                            attribute);
                }
                Node finder = addFinder(doc, attribute, "false", false);
                attribute(finder, "name", "findBy" + capitalize(fieldName) + "Id");
                String staticName = "FIND_BY";
                for (String part : splitByCharacterTypeCamelCase(fieldName + "Id"))
                    staticName += "_" + part.toUpperCase();
                attribute(finder, "listName", staticName + "_LIST");
                attribute(finder, "tableName", staticName + "_TABLE");
            } else if (attribute.getNodeName().equals("set")) {
                String relatedObjectName = attribute.getAttributes().getNamedItem("class").getNodeValue();
                attribute(attribute, "multiplicity", "1-to-n");
                if (relatedObjectName.equals(modelName)) {
                    attribute(attribute, "pigsear", "true");
                } else {
                    attribute(attribute, "pigsear", "false");
                }

                String multiplicity = attribute.getAttributes().getNamedItem("multiplicity").getNodeValue();
                if ("1-to-n".equals(multiplicity)) {
                    attribute(attribute, "cascade", "true");
                    if (sortedModels.contains(relatedObjectName)) {
                        attribute(attribute, "sorted", "true");
                        attribute(attribute, "className", "PersistableSortedList<" + relatedObjectName + ">");
                    } else {
                        attribute(attribute, "className", "PersistableList<" + relatedObjectName + ">");
                    }
                } else {
                    attribute(attribute, "cascade", "false");
                    String position;
                    if (modelName.compareTo(relatedObjectName) < 0) {
                        attribute(attribute, "relationTableName", modelName + "_" + relatedObjectName);
                        position = "left";
                    } else {
                        attribute(attribute, "relationTableName", relatedObjectName + "_" + modelName);
                        position = "right";
                    }
                    attribute(attribute, "position", position);
                    if (sortedModels.contains(relatedObjectName) && sortedModels.contains(modelName)) {
                        attribute(attribute, "sorted", "both");
                    } else if (sortedModels.contains(modelName)) {
                        attribute(attribute, "sorted", "me");
                    } else if (sortedModels.contains(relatedObjectName)) {
                        attribute(attribute, "sorted", "them");
                    }
                    if (sortedModels.contains(relatedObjectName)) {
                        attribute(attribute, "className", "PersistableSortedList<" + relatedObjectName + ">");
                    } else {
                        attribute(attribute, "className", "PersistableList<" + relatedObjectName + ">");
                    }
                }
                attribute(attribute, "sorted", "false");
                attribute(attribute, "myName", uncapitalize(modelName));
                attribute(attribute, "theirName", uncapitalize(relatedObjectName));
                attribute(attribute, "plural", new Inflector().pluralize(attribute.getAttributes().getNamedItem("theirName").getNodeValue()));
                attribute(attribute, "fieldName", attribute.getAttributes().getNamedItem("plural").getNodeValue());
                attribute(attribute, "lazy", "true");

                if ("n-to-m".equals(multiplicity)) {
                    String theirName = capitalize(attribute.getAttributes().getNamedItem("theirName").getNodeValue());
                    String myName = capitalize(attribute.getAttributes().getNamedItem("myName").getNodeValue());
                    //Self related n2m: need two finders
                    if (relatedObjectName.equals(modelName)) {
                        Node finder = addFinder(doc, attribute, "false", false);
                        attribute(finder, "name", "findBy" + theirName + "Id");
                        String staticName = "FIND_BY";
                        for (String part : splitByCharacterTypeCamelCase(myName + "By" + theirName + "Id"))
                            staticName += "_" + part.toUpperCase();

                        attribute(finder, "listName", staticName + "_LIST");
                        attribute(finder, "tableName", staticName + "_TABLE");

                        finder = addFinder(doc, attribute, "false", true);
                        attribute(finder, "name", "findBy" + myName + "Id");
                        staticName = "FIND_BY";
                        for (String part : splitByCharacterTypeCamelCase(myName + "By" + myName + "Id"))
                            staticName += "_" + part.toUpperCase();

                        attribute(finder, "listName", staticName + "_LIST");
                        attribute(finder, "tableName", staticName + "_TABLE");

                    } else {
                        Node finder = addFinder(doc, attribute, "false", false);
                        attribute(finder, "name", "findBy" + theirName + "Id");
                        String staticName = "FIND_BY";
                        for (String part : splitByCharacterTypeCamelCase(myName + "By" + theirName + "Id"))
                            staticName += "_" + part.toUpperCase();

                        attribute(finder, "listName", staticName + "_LIST");
                        attribute(finder, "tableName", staticName + "_TABLE");
                    }
                }
            } else if (attribute.getNodeName().equals("string")) {
                if (Integer.valueOf(attribute.getAttributes().getNamedItem("maxlength").getNodeValue()) >= 65000) {
                    attribute(attribute, "lazy", "true");
                }
            }
            //Defaults
            if (attribute.getAttributes().getNamedItem("name") != null)
                attribute(attribute, "fieldName", attribute.getAttributes().getNamedItem("name").getNodeValue());
            attribute(attribute, "className", capitalize(attribute.getNodeName()));
            attribute(attribute, "list", "false");
            attribute(attribute, "notnull", "false");
            attribute(attribute, "lazy", "false");
            Node finder = finder(attribute, null);
            if (finder != null) {
                attribute(finder, "name", "findBy" + capitalize(attribute.getAttributes().getNamedItem("fieldName").getNodeValue()));
                attribute(finder, "json", "false");
                String staticName = "FIND_BY";
                for (String part : splitByCharacterTypeCamelCase(attribute.getAttributes().getNamedItem("fieldName").getNodeValue() + "Id"))
                    staticName += "_" + part.toUpperCase();
                attribute(finder, "listName", staticName + "_LIST");
                attribute(finder, "tableName", staticName + "_TABLE");
            }
        }
    }

    /**
     * There are two query types: the finder query is a query that will be added to Finder and
     * returns a collection of pojo's. The other type is an action query that can have any return type.
     * The latter is simply add as static String to SQLBase.
     *
     * @throws ModelGenerationException
     */
    void normalizeQueries() throws ModelGenerationException {
        NodeList queries;
        try {
            queries = queries(doc);
        } catch (XPathExpressionException e) {
            DocPrint.print(doc);
            throw new ModelGenerationException("Could not get the queries", e);
        }
        for (int i = 0; i < queries.getLength(); i++) {
            Node query = queries.item(i);
            String staticName = "QUERY_BY";
            for (String part : splitByCharacterTypeCamelCase(query.getAttributes().getNamedItem("name").getNodeValue()))
                staticName += "_" + part.toUpperCase();

            Node finder = finder(query, null);
            if (finder != null) {
                attribute(finder, "name", "QueryBy" + capitalize(query.getAttributes().getNamedItem("name").getNodeValue()));
                attribute(finder, "json", "false");
                attribute(finder, "listName", staticName + "_LIST");
                attribute(finder, "tableName", staticName + "_TABLE");
            } else {
                attribute(query, "sqlName", staticName.toString());
            }
            for (int j = 0; j < query.getChildNodes().getLength(); j++) {
                Node defaultNode = query.getChildNodes().item(j);
                if ("default".equals(defaultNode.getNodeName())) {
                    Node listNode = doc.createElement("list");
                    listNode.setTextContent(defaultNode.getTextContent());
                    query.appendChild(listNode);
                    Node tableNode = doc.createElement("table");
                    tableNode.setTextContent(defaultNode.getTextContent());
                    query.appendChild(tableNode);
                    break;
                }
            }
        }
    }

    /**
     * Add listScope, tableScope, formScope and lazyScope nodes to the model.<br>
     * <b>listScope</b><br>
     * In this node all the attributes that have set list='true' are added. If
     * there is no attribute with list='true' it will add the notnull='true'
     * attributes. Larger attributes: the xml and html attributes are ignored.
     * They can't be displayed in a dropdown list. <br>
     * <b>tableScope</b><br>
     * All attributes are added to the tableScope unless they are:<br>
     * - id, creationDate or updateDate;<br>
     * - set, html, xml (and not lazy='false');<br>
     * - or they have lazy='false'. <br>
     * <b>lazyScope</b><br>
     * This scope will have the attributes (excluding set and object) who are excluded from
     * the tableScope. <br>
     * <b>formScope</b><br>
     * This is lazyScope + tableScope.
     *
     * @throws ModelGenerationException Wrapper on XPath exception
     */
    public void addScope() throws ModelGenerationException {
        addChildElement(doc, "listScope", null);
        addChildElement(doc, "tableScope", null);
        addChildElement(doc, "lazyScope", null);
        addChildElement(doc, "formScope", null);
        try {
            NodeList attributes = attributes(doc, "*[@list='true']");
            boolean empty = true;
            for (int i = 0; i < attributes.getLength(); i++) {
                Node insert = attributes.item(i);
                if (insert.getNodeName().equals("html") || insert.getNodeName().equals("xml")) {
                    System.out.println("html and xml attribute types are not available for the Scope 'list'");
                } else if (insert.getNodeName().equals("object")) {
                    //Ignore object, the objectId will be added to the list
                } else if (insert.getNodeName().equals("string")
                        && Integer.parseInt(insert.getAttributes().getNamedItem("maxlength").getNodeValue()) > 255) {
                    System.out.println("Larger string attributes (maxlength > 255) are not available for the Scope 'list");
                } else {
                    doc.getElementsByTagName("listScope").item(0).appendChild(
                            doc.importNode(insert, true));
                    empty = false;
                }
            }
            if (empty) {
                attributes = attributes(doc, "*[@notnull='true']");
                for (int i = 0; i < attributes.getLength(); i++) {
                    Node insert = attributes.item(i);
                    if (insert.getNodeName().equals("html") || insert.getNodeName().equals("xml")) {
                        System.out.println("html and xml attribute types are not available for the Scope 'list'");
                    } else if (insert.getNodeName().equals("string")
                            && Integer.parseInt(insert.getAttributes().getNamedItem("maxlength").getNodeValue()) > 100) {
                        System.out.println("Larger string attibutes (maxlength > 100) are not available for the Scope 'list");
                    } else {
                        doc.getElementsByTagName("listScope").item(0).appendChild(
                                doc.importNode(insert, true));
                    }
                }
            }

            attributes = attributes(doc, null);
            for (int i = 0; i < attributes.getLength(); i++) {
                Node insert = attributes.item(i);
                if (insert.getNodeName().equals("set")) {
                    // Set is never (for now).
                } else if ("true".equals(insert.getAttributes().getNamedItem("lazy").getNodeValue())
                        && !insert.getNodeName().equals("object")) {
                    doc.getElementsByTagName("lazyScope").item(0).appendChild(
                            doc.importNode(insert, true));
                    doc.getElementsByTagName("formScope").item(0).appendChild(
                            doc.importNode(insert, true));
                } else {
                    doc.getElementsByTagName("tableScope").item(0).appendChild(
                            doc.importNode(insert, true));
                    doc.getElementsByTagName("formScope").item(0).appendChild(
                            doc.importNode(insert, true));
                }
            }
            if (!doc.getElementsByTagName("lazyScope").item(0).hasChildNodes()) {
                System.out.println("There is no lazy field, remove the lazyScope node");
                doc.getFirstChild().removeChild(doc.getElementsByTagName("lazyScope").item(0));
            }
            if (!doc.getElementsByTagName("listScope").item(0).hasChildNodes()) {
                System.out.println("There is no list field for dropdown list, i removed the listScope node");
                doc.getFirstChild().removeChild(doc.getElementsByTagName("listScope").item(0));
            }
        } catch (XPathExpressionException e) {
            DocPrint.print(doc);
            throw new ModelGenerationException("Failed to parse the attributes in this model", e);
        }

        doc.normalize();
    }

    /**
     * Add hasLazy true/false to the model. True when there are lazy=true attributes. This excludes set (which is not a
     * real attribute).
     *
     * @throws ModelGenerationException Wrapper on XPath exception
     */
    private void addLazy() throws ModelGenerationException {
        try {
            int lazy = attributes(doc, "*[@lazy='true']").getLength()
                    - attributes(doc, "set[@lazy='true']").getLength();
            addChildElement(doc, "hasLazy", lazy > 0 ? "true" : "false");
            System.out.println(modelName + " has " + lazy + " lazy attributes");
        } catch (XPathExpressionException e) {
            throw new ModelGenerationException("Can't evaluate lazy='true' xpath", e);
        }
    }
}
